﻿using System;
using System.Collections.Generic;
using System.Linq;
using DependencyAnalysis.DataModel;
using Microsoft.CSharp.RuntimeBinder;

namespace DependencyAnalysis.Serialization
{
    public class CodeSmellReport : IReport
    {
        public Setup Setup { get; set; }

        public IReportObject FetchReport<T>() where T : IReportObject, new()
        {
            if (Setup == null) throw new InvalidOperationException();
            IReportObject ireport = new T();
            dynamic report = ireport;
            report.Name = "Code Smell";
            report.TypeName = "Report";

            var smellFromDuplicates = new Dictionary<MemberDescriptor, int>();
            FillSmellFromDuplicates(smellFromDuplicates);

            var smellFromMember = new Dictionary<TypeDescriptor, int>();
            report.Members = FillSmell<TypeDescriptor, MemberDescriptor, T>(new FillSmellParameters<TypeDescriptor, MemberDescriptor>
            {
                NextLevel = smellFromMember,
                PropagatedSmell = smellFromDuplicates,
                PropagatedValueName = "Duplication",
                PropagatedTypeName = "Member",
                Filter = Filters.MyMembers,
                Metric = DoesMemberSmell<T>
            });

            var smellFromType = new Dictionary<SubmoduleDescriptor, int>();
            report.Types = FillSmell<SubmoduleDescriptor, TypeDescriptor, T>(new FillSmellParameters<SubmoduleDescriptor, TypeDescriptor>
            {
                NextLevel = smellFromType,
                PropagatedSmell = smellFromMember,
                PropagatedValueName = "FromMembers",
                PropagatedTypeName = "Type",
                Filter = Filters.MyTypes,
                Metric = DoesTypeSmell<T>
            });

            var smellFromSubmodule = new Dictionary<ModuleDescriptor, int>();
            var submodules = FillSmell<ModuleDescriptor, SubmoduleDescriptor, T>(new FillSmellParameters<ModuleDescriptor, SubmoduleDescriptor>
            {
                NextLevel = smellFromSubmodule,
                PropagatedSmell = smellFromType,
                PropagatedValueName = "FromTypes",
                PropagatedTypeName = "Submodule",
                Filter = Filters.MySubmodules,
                Metric = DoesSubmoduleSmell<T>
            });
            report.Submodules = submodules;

            FillSmellFromCyclicDependencies(submodules);
            FillSmellFromInstableDependencies<SubmoduleDescriptor, T>(submodules);

            var smellFromModule = new Dictionary<ModuleDescriptor, int>();
            var modules = FillSmell<ModuleDescriptor, ModuleDescriptor, T>(new FillSmellParameters<ModuleDescriptor, ModuleDescriptor>
            {
                NextLevel = smellFromModule,
                PropagatedSmell = smellFromSubmodule,
                PropagatedValueName = "FromSubmodules",
                PropagatedTypeName = "Module",
                Filter = Filters.MyModules,
                Metric = DoesModuleSmell<T>
            });
            report.Modules = modules;
            FillSmellFromInstableDependencies<ModuleDescriptor, T>(modules);

            report.SmellPoints = SumTotalSmell(ireport);
            return report;
        }

        private static int SumTotalSmell(IReportObject ireport)
        {
            dynamic report = ireport;
            int totalSmell = 0;
            foreach (dynamic x in report.Modules)
            {
                try
                {
                    totalSmell += x.Metrics.SmellPoints;
                }
                catch (RuntimeBinderException)
                {
                }
            }
            return totalSmell;
        }

        private void FillSmellFromInstableDependencies<TDescriptor, T>(IList<T> reportItems)
            where TDescriptor : IDescriptor
            where T : IReportObject, new()
        {
            var instableDependencies = Principles.StableDependencies<TDescriptor>(Setup.Model);
            foreach (var dependency in instableDependencies)
            {
                var submodule = dependency.Item1;
                dynamic reportItem = reportItems.First(x => Equals(x.Name, submodule.FullName));
                try
                {
                    reportItem.SmellPoints++;
                }
                catch (RuntimeBinderException)
                {

                    reportItem.SmellPoints = 1;
                }
            }
        }

        private void FillSmellFromCyclicDependencies<T>(IList<T> submodules) where T : IReportObject, new()
        {
            var cycles = Principles.AcyclicDependencies(Setup.Model);
            foreach (var cycle in cycles)
            {
                foreach (var submodule in cycle)
                {
                    dynamic reportItem = submodules.First(x => Equals(x.Name, submodule.FullName));
                    try
                    {
                        reportItem.SmellPoints++;
                    }
                    catch (RuntimeBinderException)
                    {

                        reportItem.SmellPoints = 1;
                    }
                }
            }
        }

        private class FillSmellParameters<TNext, TCurrent>
            where TNext : IDescriptor
            where TCurrent : IDescriptor
        {
            public Dictionary<TNext, int> NextLevel;
            public Dictionary<TCurrent, int> PropagatedSmell;
            public string PropagatedValueName;
            public string PropagatedTypeName;
            public Predicate<TCurrent> Filter;
            public Func<TCurrent, State, bool> Metric;
        }

        private List<T> FillSmell<TNext, TCurrent, T>(FillSmellParameters<TNext, TCurrent> parameters)
            where TNext : IDescriptor
            where TCurrent : IDescriptor
            where T : IReportObject, new()
        {
            var currentLevel = new List<T>();
            var smellingItems = Setup.Verifier.Report(parameters.Metric, parameters.Filter);
            AddExpandoObjectBySmellingItem(parameters, smellingItems, currentLevel);
            AddExpandoObjectByPropagatedSmell(parameters, currentLevel);
            parameters.PropagatedSmell.Clear();
            return currentLevel;
        }

        private static void AddExpandoObjectByPropagatedSmell<TNext, TCurrent, T>(FillSmellParameters<TNext, TCurrent> parameters,
                                                                         List<T> currentLevel)
            where TNext : IDescriptor
            where TCurrent : IDescriptor
            where T : IReportObject, new()
        {
            foreach (var propagated in parameters.PropagatedSmell)
            {
                dynamic reportObject = new T
                    {
                        Name = propagated.Key.FullName,
                        TypeName = parameters.PropagatedTypeName
                    };
                dynamic metrics = new T { TypeName = "Metrics" };
                metrics.SmellPoints = propagated.Value;
                metrics.Set(parameters.PropagatedValueName, propagated.Value);
                reportObject.Metrics = metrics;
                var nextLevelItem = propagated.Key.Parent;
                AddSmellPointsToNextLevel(parameters.NextLevel, nextLevelItem, propagated.Value);
                currentLevel.Add(reportObject);
            }
        }

        private static void AddExpandoObjectBySmellingItem<TNext, TCurrent, T>(FillSmellParameters<TNext, TCurrent> parameters, IList<Tuple<TCurrent, State>> smellingItems,
                                                                            List<T> currentLevel)
            where TNext : IDescriptor
            where TCurrent : IDescriptor
            where T : IReportObject, new()
        {
            foreach (var smellingItem in smellingItems)
            {
                dynamic reportObject = new T
                    {
                        Name = smellingItem.Item1.FullName,
                        TypeName = parameters.PropagatedTypeName
                    };
                dynamic metrics = smellingItem.Item2.Value;
                reportObject.Metrics = smellingItem.Item2.Value;
                if (parameters.PropagatedSmell.ContainsKey(smellingItem.Item1))
                {
                    var propagated = parameters.PropagatedSmell[smellingItem.Item1];
                    metrics.SmellPoints += propagated;
                    metrics.Set(parameters.PropagatedValueName, propagated);
                    parameters.PropagatedSmell.Remove(smellingItem.Item1);
                }

                int smellPoints = metrics.SmellPoints;
                var nextLevelItem = smellingItem.Item1.Parent;
                AddSmellPointsToNextLevel(parameters.NextLevel, nextLevelItem, smellPoints);
                currentLevel.Add(reportObject);
            }
        }

        private static void AddSmellPointsToNextLevel<TNext>(Dictionary<TNext, int> nextLevel, IDescriptor nextLevelItem,
                                                                       int smellPoints) where TNext : IDescriptor
        {
            int sum = 0;
            if (nextLevelItem != null)
            {
                if (nextLevel.ContainsKey((TNext)nextLevelItem))
                {
                    sum = nextLevel[(TNext)nextLevelItem];
                }
                nextLevel[(TNext)nextLevelItem] = sum + smellPoints;
            }
        }

        private void FillSmellFromDuplicates(Dictionary<MemberDescriptor, int> smellFromDuplicates)
        {
            var duplicates = Principles.DontRepeatYourself(Setup.Model, 40);
            foreach (var duplicate in duplicates)
            {
                foreach (var member in duplicate.Members)
                {
                    int sum = 0;
                    if (smellFromDuplicates.ContainsKey(member))
                    {
                        sum = smellFromDuplicates[member];
                    }
                    smellFromDuplicates[member] = sum + 1;
                }
            }
        }

        private bool DoesTypeSmell<T>(TypeDescriptor x, State state) where T : IReportObject, new()
        {
            var lackOfCohesion = Setup.Metrics.LackOfCohesion(x);
            var fields = Setup.Metrics.NumberOfFields(x);
            var methods = Setup.Metrics.NumberOfMethods(x);
            var instructions = Setup.Metrics.NumberOfInstructions(x);
            var complexity = Setup.Metrics.CyclomaticComplexity(x);
            var specialization = Setup.Metrics.SpecializationIndex(x);
            dynamic stateObject = new T { TypeName = "Metrics" };
            state.Value = stateObject;
            int smellPoints = 0;

            stateObject.LackOfCohesion = lackOfCohesion;
            stateObject.NumberOfFields = fields;
            stateObject.NumberOfInstructions = instructions;
            stateObject.NumberOfMethods = methods;
            stateObject.CyclomaticComplexity = complexity;

            if (lackOfCohesion > 0.95 && methods >= 10 && fields >= 10)
            {
                smellPoints++;
            }
            if (fields > 20 && !x.IsEnum)
            {
                smellPoints++;
            }
            if (instructions > 2000)
            {
                smellPoints++;
            }
            if (methods > 20)
            {
                smellPoints++;
            }
            if (complexity > 200)
            {
                smellPoints++;
            }
            if (specialization > 1.2)
            {
                smellPoints++;
            }
            if (smellPoints == 0) return false;
            stateObject.SmellPoints = smellPoints;
            return true;
        }

        private bool DoesSubmoduleSmell<T>(SubmoduleDescriptor x, State state) where T : IReportObject, new()
        {
            var distance = Math.Abs(Setup.Metrics.DistanceFromMainSequence(x));
            var cohesion = Setup.Metrics.RelationalCohesion(x);
            dynamic stateObject = new T { TypeName = "Metrics" };
            state.Value = stateObject;

            stateObject.DistanceFromMainSequence = distance;
            stateObject.RelationalCohesion = cohesion;

            int smellPoints = 0;
            if (distance > 0.8)
            {
                smellPoints++;
            }
            if (cohesion > 4.0 || cohesion < 1.5)
            {
                smellPoints++;
            }
            stateObject.SmellPoints = smellPoints;
            return true;
        }

        private bool DoesModuleSmell<T>(ModuleDescriptor x, State state) where T : IReportObject, new()
        {
            var distance = Math.Abs(Setup.Metrics.DistanceFromMainSequence(x));
            var cohesion = Setup.Metrics.RelationalCohesion(x);
            dynamic stateObject = new T { TypeName = "Metrics" };
            state.Value = stateObject;

            stateObject.DistanceFromMainSequence = distance;
            stateObject.RelationalCohesion = cohesion;

            int smellPoints = 0;
            if (distance > 0.8)
            {
                smellPoints++;
            }
            if (cohesion > 4.0 || cohesion < 1.5)
            {
                smellPoints++;
            }
            stateObject.SmellPoints = smellPoints;
            return true;
        }

        private bool DoesMemberSmell<T>(MemberDescriptor x, State state) where T : IReportObject, new()
        {
            var nestingDepth = Setup.Metrics.NestingDepth(x);
            var complexity = Setup.Metrics.CyclomaticComplexity(x);
            var instructions = Setup.Metrics.NumberOfInstructions(x);
            var variables = Setup.Metrics.NumberOfVariables(x);
            var parameters = Setup.Metrics.NumberOfParameters(x);
            dynamic stateObject = new T { TypeName = "Metrics" };
            state.Value = stateObject;

            stateObject.NumberOfParameters = parameters;
            stateObject.NumberOfVariables = variables;
            stateObject.NumberOfInstructions = instructions;
            stateObject.CyclomaticComplexity = complexity;
            stateObject.NestingDepth = nestingDepth;

            int smellPoints = 0;
            if (!x.IsOverride && !x.IsDefinedByInterface && parameters > 3)
            {
                smellPoints++;
            }
            if (variables > 8)
            {
                smellPoints++;
            }
            if (instructions > 200)
            {
                smellPoints++;
            }
            if (complexity > 20)
            {
                smellPoints++;
            }
            if (nestingDepth > 5)
            {
                smellPoints++;
            }
            if (smellPoints == 0) return false;
            stateObject.SmellPoints = smellPoints;
            return true;
        }

    }
}